Profile picture

[Python] ORM 기반 ToDo 프로젝트 만들어보기

JaehyoJJAng2025년 11월 10일

개요

만들 것

  • 단일 테이블(Todo)만 사용함.
  • 기본 CRUD만 집중

배울 것

  • MySQL 연결 설정
  • ORM 모델 정의
  • 기본 CRUD 작업
  • API 개발 및 테스트 과정

실습에 사용될 기술 스택

  • 백엔드: Flask, SQLAlchemy
  • 데이터베이스: MySQL
  • 프론트엔드: 간단한 HTML

📁 프로젝트 구조

todo_app/
├── app.py                 # Flask 애플리케이션
├── models.py              # ORM 모델
├── database.py            # MySQL 연결 설정
├── config.py              # 데이터베이스 설정
├── requirements.txt       # 패키지 목록
└── templates/
    └── index.html         # 웹 UI

1. MySQL 준비

MySQL에 접속해서 데이터베이스를 만들어주겠습니다.

mysql -uroot -p
-- 데이터베이스 생성
CREATE DATABASE todo_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

-- 사용자 생성 (선택사항)
CREATE USER 'todo_user'@'localhost' IDENTIFIED BY 'your_password';

-- 권한 부여
GRANT ALL PRIVILEGES ON todo_db.* TO 'todo_user'@'localhost';

-- 권한 적용
FLUSH PRIVILEGES;

-- 확인
SHOW DATABASES;

-- 나가기
EXIT;

2. 패키지 설치

실습에 필요한 파이썬 패키지들을 설치할게요.

requirements.txt

Flask==3.0.0
Flask-SQLAlchemy==3.1.1
PyMySQL==1.1.0
cryptography==41.0.7

아래 명령어로 설치를 진행해주세요.

pip install -r requirements.txt

3. 데이터베이스 설정

데이터베이스 연결을 위한 config를 정의하는 파일을 생성해주도록 하겠습니다.


config.py

"""
데이터베이스 설정 파일

MySQL 연결 정보를 관리합니다.
실제 운영에서는 환경변수나 .env 파일을 사용하세요!
"""
# ============================================
# MySQL 연결 정보
# ============================================

# MySQL 서버 정보
MYSQL_HOST = 'localhost'      # MySQL 서버 주소
MYSQL_PORT = 3306             # MySQL 포트 (기본값: 3306)
MYSQL_USER = 'todo_user'      # MySQL 사용자 이름
MYSQL_PASSWORD = 'your_password'  # MySQL 비밀번호
MYSQL_DATABASE = 'todo_db'    # 데이터베이스 이름

# ============================================
# SQLAlchemy 설정
# ============================================

class Config:
    """Flask 애플리케이션 설정"""

    # MySQL 연결 URL 생성
    # 형식: mysql+pymysql://사용자:비밀번호@호스트:포트/데이터베이스?charset=utf8mb4
    SQLALCHEMY_DATABASE_URI = (
        f'mysql+pymysql://{MYSQL_USER}:{MYSQL_PASSWORD}'
        f'@{MYSQL_HOST}:{MYSQL_PORT}/{MYSQL_DATABASE}'
        f'?charset=utf8mb4'
    )

    # SQLAlchemy 설정
    SQLALCHEMY_TRACK_MODIFICATIONS = False  # 성능 향상
    SQLALCHEMY_ECHO = True  # SQL 쿼리 로그 출력 (학습용)

    # Flask 설정
    SECRET_KEY = 'dev-secret-key-change-in-production'
    DEBUG = True
  • 실제 운영에서는 비밀번호를 코드에 직접 쓰지 마세요!
  • .env 파일이나 환경변수를 사용하세요
  • config.py.gitignore에 추가하세요

database.py

"""
데이터베이스 연결 및 초기화

Flask-SQLAlchemy를 사용하여 MySQL과 연결합니다.
"""

from flask_sqlalchemy import SQLAlchemy

# ============================================
# SQLAlchemy 인스턴스 생성
# ============================================

# db 객체 생성
# 이 객체를 통해 모든 데이터베이스 작업을 수행합니다
db = SQLAlchemy()

def init_db(app):
    """
    데이터베이스 초기화

    Flask 앱과 SQLAlchemy를 연결하고 테이블을 생성합니다.

    Args:
        app: Flask 애플리케이션 인스턴스
    """

    print("\n" + "=" * 60)
    print("데이터베이스 초기화 중...")
    print("=" * 60)

    # Flask 앱과 SQLAlchemy 연결
    db.init_app(app)

    # 애플리케이션 컨텍스트 내에서 테이블 생성
    with app.app_context():
        # 모든 모델의 테이블 생성
        # 이미 존재하는 테이블은 건드리지 않음
        db.create_all()

        print("\n✅ 데이터베이스 테이블 생성 완료!")
        print("=" * 60 + "\n")


def drop_all_tables(app):
    """
    모든 테이블 삭제 (개발용)

    Args:
        app: Flask 애플리케이션 인스턴스
    """

    print("\n⚠️  주의: 모든 테이블을 삭제합니다!")

    with app.app_context():
        db.drop_all()
        print("✅ 모든 테이블이 삭제되었습니다.\n")
  • SQLAlchemy: 순수 SQLAlchemy (이전 프로젝트)
  • Flask-SQLAlchemy: Flask와 통합된 버전
  • Flask-SQLAlchemy가 Flask에서 더 편리함

4. ORM 모델 정의 (테이블 스키마 정의)

models.py

"""
ORM 모델 정의

Todo 테이블 하나만 사용하는 간단한 구조입니다.
"""

from database import db
from datetime import datetime


# ============================================
# Todo 모델 (할 일)
# ============================================

class Todo(db.Model):
    """
    할 일 테이블

    단일 테이블로 관계 없음!
    각 할 일은 독립적으로 존재합니다.
    """

    # 테이블 이름 지정
    __tablename__ = 'todos'

    # ===== 컬럼 정의 =====

    # id: 기본 키 (Primary Key)
    # Integer: 정수형
    # primary_key=True: 기본 키로 설정
    # autoincrement=True: 자동 증가 (MySQL의 AUTO_INCREMENT)
    id = db.Column(db.Integer, primary_key=True, autoincrement=True)

    # title: 할 일 제목
    # String(200): 최대 200자 문자열
    # nullable=False: NULL 불가 (필수 입력)
    title = db.Column(db.String(200), nullable=False)

    # description: 할 일 설명 (선택사항)
    # Text: 긴 텍스트
    # nullable=True: NULL 가능 (기본값이므로 생략 가능)
    description = db.Column(db.Text, nullable=True)

    # completed: 완료 여부
    # Boolean: True/False
    # default=False: 기본값은 False (미완료)
    completed = db.Column(db.Boolean, default=False, nullable=False)

    # priority: 우선순위
    # Integer: 1(낮음), 2(보통), 3(높음)
    # default=2: 기본값은 2 (보통)
    priority = db.Column(db.Integer, default=2, nullable=False)

    # due_date: 마감일 (선택사항)
    # DateTime: 날짜와 시간
    due_date = db.Column(db.DateTime, nullable=True)

    # created_at: 생성 시간
    # default=datetime.utcnow: 생성 시 자동으로 현재 시간 설정
    created_at = db.Column(db.DateTime, default=datetime.utcnow, nullable=False)

    # updated_at: 수정 시간
    # onupdate=datetime.utcnow: 수정 시 자동으로 현재 시간으로 갱신
    updated_at = db.Column(
        db.DateTime,
        default=datetime.utcnow,
        onupdate=datetime.utcnow,
        nullable=False
    )

    def __repr__(self):
        """
        객체를 문자열로 표현

        print(todo) 했을 때 출력되는 내용
        디버깅할 때 유용함
        """
        return f"<Todo(id={self.id}, title='{self.title}', completed={self.completed})>"

    def to_dict(self):
        """
        객체를 딕셔너리로 변환

        JSON 응답을 만들 때 사용

        Returns:
            dict: Todo 정보가 담긴 딕셔너리
        """
        return {
            'id': self.id,
            'title': self.title,
            'description': self.description,
            'completed': self.completed,
            'priority': self.priority,
            'priority_text': self.get_priority_text(),  # 우선순위 텍스트
            'due_date': self.due_date.isoformat() if self.due_date else None,
            'created_at': self.created_at.isoformat() if self.created_at else None,
            'updated_at': self.updated_at.isoformat() if self.updated_at else None,
        }

    def get_priority_text(self):
        """
        우선순위를 텍스트로 변환

        Returns:
            str: 우선순위 텍스트 ('낮음', '보통', '높음')
        """
        priority_map = {
            1: '낮음',
            2: '보통',
            3: '높음'
        }
        return priority_map.get(self.priority, '보통')

컬럼 타입 정리

SQLAlchemy MySQL 설명
Integer INT 정수
String(길이) VARCHAR(길이) 가변 길이 문자열
Text TEXT 긴 텍스트
Boolean TINYINT(1) True/False
DateTime DATETIME 날짜와 시간
Float FLOAT 부동소수점
Numeric DECIMAL 정밀한 소수

5. Flask API 구현

app.py

"""
Flask 애플리케이션 - Todo API

간단한 CRUD API를 제공합니다.
"""

from flask import Flask, jsonify, request, render_template
from config import Config
from database import db, init_db
from models import Todo
from datetime import datetime

# ============================================
# Flask 앱 생성 및 설정
# ============================================

app = Flask(__name__)

# 설정 로드
app.config.from_object(Config)

# 데이터베이스 초기화
init_db(app)


# ============================================
# 웹 페이지
# ============================================

@app.route('/')
def index():
    """
    메인 페이지

    모든 할 일을 표시합니다.
    """

    # 모든 할 일 조회
    # order_by(): 정렬
    # - created_at.desc(): 생성 시간 내림차순 (최신이 위로)
    todos = Todo.query.order_by(Todo.created_at.desc()).all()

    # 통계 계산
    total_count = len(todos)
    completed_count = len([t for t in todos if t.completed])
    pending_count = total_count - completed_count

    return render_template(
        'index.html',
        todos=todos,
        total_count=total_count,
        completed_count=completed_count,
        pending_count=pending_count
    )


# ============================================
# API 엔드포인트
# ============================================

# ----- 조회 (READ) -----

@app.route('/api/todos', methods=['GET'])
def api_get_todos():
    """
    모든 할 일 조회 API

    GET /api/todos
    Query Parameters:
        - completed: true/false (완료 여부로 필터링)
        - priority: 1/2/3 (우선순위로 필터링)

    Returns:
        JSON: 할 일 목록
    """

    # 쿼리 시작
    query = Todo.query

    # 완료 여부로 필터링 (선택)
    completed = request.args.get('completed')
    if completed is not None:
        # 'true' 문자열을 Boolean으로 변환
        is_completed = completed.lower() == 'true'
        query = query.filter(Todo.completed == is_completed)

    # 우선순위로 필터링 (선택)
    priority = request.args.get('priority', type=int)
    if priority:
        query = query.filter(Todo.priority == priority)

    # 정렬 및 실행
    todos = query.order_by(Todo.created_at.desc()).all()

    # 딕셔너리 리스트로 변환
    return jsonify([todo.to_dict() for todo in todos])


@app.route('/api/todos/<int:todo_id>', methods=['GET'])
def api_get_todo(todo_id):
    """
    특정 할 일 조회 API

    GET /api/todos/{todo_id}

    Returns:
        JSON: 할 일 정보
    """

    # get_or_404(): ID로 조회, 없으면 자동으로 404 에러
    todo = Todo.query.get_or_404(todo_id)

    return jsonify(todo.to_dict())


# ----- 생성 (CREATE) -----

@app.route('/api/todos', methods=['POST'])
def api_create_todo():
    """
    할 일 생성 API

    POST /api/todos
    Body: {
        "title": "할 일 제목" (필수),
        "description": "설명" (선택),
        "priority": 1-3 (선택, 기본값: 2),
        "due_date": "2024-12-31T23:59:59" (선택)
    }

    Returns:
        JSON: 생성된 할 일 정보
    """

    # 요청 데이터 가져오기
    data = request.get_json()

    # 유효성 검사
    if not data:
        return jsonify({'error': '데이터가 없습니다.'}), 400

    if not data.get('title'):
        return jsonify({'error': '제목은 필수입니다.'}), 400

    # 우선순위 검증
    priority = data.get('priority', 2)
    if priority not in [1, 2, 3]:
        return jsonify({'error': '우선순위는 1, 2, 3 중 하나여야 합니다.'}), 400

    # 마감일 파싱 (선택)
    due_date = None
    if data.get('due_date'):
        try:
            # ISO 8601 형식 파싱
            due_date = datetime.fromisoformat(data['due_date'])
        except ValueError:
            return jsonify({'error': '날짜 형식이 올바르지 않습니다. (ISO 8601 형식)'}), 400

    try:
        # Todo 객체 생성
        todo = Todo(
            title=data['title'],
            description=data.get('description'),
            priority=priority,
            due_date=due_date
        )

        # 데이터베이스에 추가
        db.session.add(todo)

        # 커밋 (실제 저장)
        db.session.commit()

        print(f"✅ Todo 생성: {todo}")

        # 생성된 Todo 반환
        return jsonify(todo.to_dict()), 201

    except Exception as e:
        # 에러 발생 시 롤백
        db.session.rollback()

        print(f"❌ Todo 생성 실패: {e}")

        return jsonify({'error': str(e)}), 500


# ----- 수정 (UPDATE) -----

@app.route('/api/todos/<int:todo_id>', methods=['PUT', 'PATCH'])
def api_update_todo(todo_id):
    """
    할 일 수정 API

    PUT/PATCH /api/todos/{todo_id}
    Body: {
        "title": "새 제목" (선택),
        "description": "새 설명" (선택),
        "completed": true (선택),
        "priority": 3 (선택),
        "due_date": "2024-12-31T23:59:59" (선택)
    }

    Returns:
        JSON: 수정된 할 일 정보
    """

    # Todo 조회
    todo = Todo.query.get_or_404(todo_id)

    # 요청 데이터 가져오기
    data = request.get_json()

    if not data:
        return jsonify({'error': '데이터가 없습니다.'}), 400

    try:
        # 수정할 필드 업데이트

        if 'title' in data:
            todo.title = data['title']

        if 'description' in data:
            todo.description = data['description']

        if 'completed' in data:
            todo.completed = bool(data['completed'])

        if 'priority' in data:
            priority = data['priority']
            if priority not in [1, 2, 3]:
                return jsonify({'error': '우선순위는 1, 2, 3 중 하나여야 합니다.'}), 400
            todo.priority = priority

        if 'due_date' in data:
            if data['due_date']:
                try:
                    todo.due_date = datetime.fromisoformat(data['due_date'])
                except ValueError:
                    return jsonify({'error': '날짜 형식이 올바르지 않습니다.'}), 400
            else:
                todo.due_date = None

        # 커밋
        db.session.commit()

        print(f"✅ Todo 수정: {todo}")

        return jsonify(todo.to_dict())

    except Exception as e:
        db.session.rollback()

        print(f"❌ Todo 수정 실패: {e}")

        return jsonify({'error': str(e)}), 500


# ----- 삭제 (DELETE) -----

@app.route('/api/todos/<int:todo_id>', methods=['DELETE'])
def api_delete_todo(todo_id):
    """
    할 일 삭제 API

    DELETE /api/todos/{todo_id}

    Returns:
        JSON: 성공 메시지
    """

    # Todo 조회
    todo = Todo.query.get_or_404(todo_id)

    try:
        # 삭제
        db.session.delete(todo)
        db.session.commit()

        print(f"✅ Todo 삭제: ID {todo_id}")

        return jsonify({'message': '할 일이 삭제되었습니다.'}), 200

    except Exception as e:
        db.session.rollback()

        print(f"❌ Todo 삭제 실패: {e}")

        return jsonify({'error': str(e)}), 500


# ----- 완료 토글 (편의 기능) -----

@app.route('/api/todos/<int:todo_id>/toggle', methods=['POST'])
def api_toggle_todo(todo_id):
    """
    할 일 완료 상태 토글 API

    POST /api/todos/{todo_id}/toggle

    완료 ↔ 미완료 전환

    Returns:
        JSON: 수정된 할 일 정보
    """

    todo = Todo.query.get_or_404(todo_id)

    try:
        # 완료 상태 반전
        todo.completed = not todo.completed

        db.session.commit()

        status = "완료" if todo.completed else "미완료"
        print(f"✅ Todo 상태 변경: ID {todo_id}{status}")

        return jsonify(todo.to_dict())

    except Exception as e:
        db.session.rollback()

        print(f"❌ Todo 상태 변경 실패: {e}")

        return jsonify({'error': str(e)}), 500


# ----- 통계 API -----

@app.route('/api/stats', methods=['GET'])
def api_stats():
    """
    통계 API

    GET /api/stats

    Returns:
        JSON: 통계 정보
    """

    # 전체 개수
    total = Todo.query.count()

    # 완료된 개수
    completed = Todo.query.filter(Todo.completed == True).count()

    # 미완료 개수
    pending = total - completed

    # 우선순위별 개수
    priority_counts = {
        'high': Todo.query.filter(Todo.priority == 3).count(),
        'medium': Todo.query.filter(Todo.priority == 2).count(),
        'low': Todo.query.filter(Todo.priority == 1).count(),
    }

    return jsonify({
        'total': total,
        'completed': completed,
        'pending': pending,
        'priority_counts': priority_counts
    })


# ============================================
# 애플리케이션 실행
# ============================================

if __name__ == '__main__':
    print("\n" + "=" * 60)
    print("✅ Todo API 서버 시작")
    print("=" * 60)
    print("\n🌐 웹 UI:")
    print("   http://localhost:5000")
    print("\n🔌 API 엔드포인트:")
    print("   GET    /api/todos          # 모든 할 일 조회")
    print("   GET    /api/todos/{id}     # 특정 할 일 조회")
    print("   POST   /api/todos          # 할 일 생성")
    print("   PUT    /api/todos/{id}     # 할 일 수정")
    print("   DELETE /api/todos/{id}     # 할 일 삭제")
    print("   POST   /api/todos/{id}/toggle  # 완료 상태 토글")
    print("   GET    /api/stats          # 통계")
    print("\n" + "=" * 60 + "\n")

    app.run(debug=True, host='0.0.0.0', port=5000)

6. 웹 UI

templates/index.html

<!DOCTYPE html>
<html lang="ko">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Todo 리스트</title>
    <style>
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }

        body {
            font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
            background: #f5f7fa;
            padding: 20px;
        }

        .container {
            max-width: 800px;
            margin: 0 auto;
        }

        h1 {
            color: #2c3e50;
            margin-bottom: 30px;
            text-align: center;
        }

        .stats {
            display: flex;
            gap: 15px;
            margin-bottom: 30px;
        }

        .stat-card {
            flex: 1;
            background: white;
            padding: 15px;
            border-radius: 8px;
            text-align: center;
        }

        .stat-card h3 {
            color: #7f8c8d;
            font-size: 12px;
            margin-bottom: 8px;
        }

        .stat-card .number {
            font-size: 24px;
            font-weight: bold;
            color: #3498db;
        }

        .todo-form {
            background: white;
            padding: 20px;
            border-radius: 8px;
            margin-bottom: 20px;
        }

        .form-group {
            margin-bottom: 15px;
        }

        label {
            display: block;
            margin-bottom: 5px;
            color: #2c3e50;
            font-weight: 500;
        }

        input, textarea, select {
            width: 100%;
            padding: 10px;
            border: 1px solid #ddd;
            border-radius: 5px;
            font-size: 14px;
        }

        textarea {
            resize: vertical;
            min-height: 60px;
        }

        button {
            background: #3498db;
            color: white;
            border: none;
            padding: 10px 20px;
            border-radius: 5px;
            cursor: pointer;
            font-size: 14px;
        }

        button:hover {
            background: #2980b9;
        }

        .todo-list {
            list-style: none;
        }

        .todo-item {
            background: white;
            padding: 15px;
            margin-bottom: 10px;
            border-radius: 8px;
            display: flex;
            align-items: center;
            gap: 15px;
        }

        .todo-item.completed {
            opacity: 0.6;
        }

        .todo-checkbox {
            width: 20px;
            height: 20px;
            cursor: pointer;
        }

        .todo-content {
            flex: 1;
        }

        .todo-title {
            font-size: 16px;
            font-weight: 500;
            color: #2c3e50;
        }

        .todo-item.completed .todo-title {
            text-decoration: line-through;
        }

        .todo-description {
            color: #7f8c8d;
            font-size: 14px;
            margin-top: 5px;
        }

        .priority-badge {
            padding: 4px 8px;
            border-radius: 12px;
            font-size: 11px;
            font-weight: bold;
        }

        .priority-high {
            background: #e74c3c;
            color: white;
        }

        .priority-medium {
            background: #f39c12;
            color: white;
        }

        .priority-low {
            background: #95a5a6;
            color: white;
        }

        .delete-btn {
            background: #e74c3c;
            padding: 8px 15px;
        }

        .delete-btn:hover {
            background: #c0392b;
        }

        .empty-message {
            text-align: center;
            color: #7f8c8d;
            padding: 40px;
            background: white;
            border-radius: 8px;
        }
    </style>
</head>
<body>
    <div class="container">
        <h1>✅ Todo 리스트</h1>

        <div class="stats">
            <div class="stat-card">
                <h3>전체</h3>
                <div class="number">{{ total_count }}</div>
            </div>
            <div class="stat-card">
                <h3>완료</h3>
                <div class="number">{{ completed_count }}</div>
            </div>
            <div class="stat-card">
                <h3>진행중</h3>
                <div class="number">{{ pending_count }}</div>
            </div>
        </div>

        <div class="todo-form">
            <form id="todoForm">
                <div class="form-group">
                    <label>할 일 *</label>
                    <input type="text" id="title" required placeholder="할 일을 입력하세요">
                </div>

                <div class="form-group">
                    <label>설명</label>
                    <textarea id="description" placeholder="상세 설명 (선택)"></textarea>
                </div>

                <div class="form-group">
                    <label>우선순위</label>
                    <select id="priority">
                        <option value="1">낮음</option>
                        <option value="2" selected>보통</option>
                        <option value="3">높음</option>
                    </select>
                </div>

                <button type="submit">추가</button>
            </form>
        </div>

        <ul class="todo-list" id="todoList">
            {% if todos %}
                {% for todo in todos %}
                <li class="todo-item {% if todo.completed %}completed{% endif %}" data-id="{{ todo.id }}">
                    <input type="checkbox" class="todo-checkbox"
                           {% if todo.completed %}checked{% endif %}
                           onchange="toggleTodo({{ todo.id }})">

                    <div class="todo-content">
                        <div class="todo-title">{{ todo.title }}</div>
                        {% if todo.description %}
                        <div class="todo-description">{{ todo.description }}</div>
                        {% endif %}
                    </div>

                    <span class="priority-badge priority-{{ 'high' if todo.priority == 3 else 'medium' if todo.priority == 2 else 'low' }}">
                        {{ todo.get_priority_text() }}
                    </span>

                    <button class="delete-btn" onclick="deleteTodo({{ todo.id }})">삭제</button>
                </li>
                {% endfor %}
            {% else %}
                <li class="empty-message">등록된 할 일이 없습니다.</li>
            {% endif %}
        </ul>
    </div>

    <script>
        // 할 일 추가
        document.getElementById('todoForm').addEventListener('submit', async (e) => {
            e.preventDefault();

            const data = {
                title: document.getElementById('title').value,
                description: document.getElementById('description').value,
                priority: parseInt(document.getElementById('priority').value)
            };

            try {
                const response = await fetch('/api/todos', {
                    method: 'POST',
                    headers: {'Content-Type': 'application/json'},
                    body: JSON.stringify(data)
                });

                if (response.ok) {
                    location.reload();
                } else {
                    alert('추가 실패');
                }
            } catch (error) {
                alert('에러 발생: ' + error);
            }
        });

        // 완료 상태 토글
        async function toggleTodo(id) {
            try {
                const response = await fetch(`/api/todos/${id}/toggle`, {
                    method: 'POST'
                });

                if (response.ok) {
                    location.reload();
                }
            } catch (error) {
                alert('에러 발생: ' + error);
            }
        }

        // 삭제
        async function deleteTodo(id) {
            if (!confirm('정말 삭제하시겠습니까?')) return;

            try {
                const response = await fetch(`/api/todos/${id}`, {
                    method: 'DELETE'
                });

                if (response.ok) {
                    location.reload();
                } else {
                    alert('삭제 실패');
                }
            } catch (error) {
                alert('에러 발생: ' + error);
            }
        }
    </script>
</body>
</html>

7. API 테스트

테스트 1: 할 일 생성하기.

curl -X POST http://localhost:5000/api/todos \
  -H "Content-Type: application/json" \
  -d '{
    "title": "ORM 학습하기",
    "description": "SQLAlchemy로 MySQL 연결 실습",
    "priority": 3
  }'
# 응답
{
  "id": 1,
  "title": "ORM 학습하기",
  "description": "SQLAlchemy로 MySQL 연결 실습",
  "completed": false,
  "priority": 3,
  "priority_text": "높음",
  "due_date": null,
  "created_at": "2024-11-22T10:30:00.123456",
  "updated_at": "2024-11-22T10:30:00.123456"
}



테스트 2: 모든 할 일 조회

curl http://localhost:5000/api/todos



테스트 3: 특정 할 일 조회

curl http://localhost:5000/api/todos/1



테스트 4: 할 일 수정

curl -X PUT http://localhost:5000/api/todos/1 \
  -H "Content-Type: application/json" \
  -d '{
    "title": "ORM 마스터하기",
    "completed": true
  }'



테스트 5: 완료된 할 일만 조회

curl "http://localhost:5000/api/todos?completed=true"



테스트 6: 통계 조회

curl http://localhost:5000/api/stats
# 응답

{
  "total": 5,
  "completed": 2,
  "pending": 3,
  "priority_counts": {
    "high": 2,
    "medium": 2,
    "low": 1
  }
}

Loading script...